package taskmaster.job;

import taskmaster.auth.User;
import taskmaster.util.TaskEnumeration;
import taskmaster.util.exception.InvalidTaskException;

import java.util.Date;
import java.util.Vector;

/**
 * taskmaster.job.Task
 * <BR><BR>
 * Represents a task in the taskmaster system.  A Job is made up of any number of Tasks.
 * Tasks are stored in TaskCollection inside a hashtable.  Task objects are used mainly for storage, and do no processing directly.
 * Processing tasks, such as changing their status and updating them is left to TaskCollection, which stores all the tasks matching
 * what the user requested and provides most of the functionality to them.
 * <BR><BR>
 * Task (as do most objects in tms) use the get* and when necessary the set* methods to access their internal properties.
 * This is done to internalize any tracking or changse that need to be made to Tasks without having to change the classes that call its methods.
 * Most of the time Tasks are instantiated (by TaskBuilder) and then read by everything else.
 * <BR><BR>
 * Taskmaster is only concerned with the latest status of a Task;
 * it ignores any earlier statuses thereby asserting that the most recent status alone is the most up to date one.
 */
public class Task implements Comparable, Cloneable
{
    /**
     * the task tag of this task, taskTag and jobTag together make this task unique.
     * <BR>matches field in DB: tms.tasks.task_tag<BR>
     * Once instantiated, this value cannot be changed.
     */
    private String taskTag = null;
    /**
     * the job tag of this task, denoting the name of the job to which this task belongs.
     * <BR>matches field in DB: tms.tasks.job_tag<BR>
     * taskTag and jobTag together make this task unique.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private String jobTag = null;
    /**
     * key used for when Task is hashed.  Unique is made from combo of jobTag & taskTag in the pattern jobTag:taskTag.
     * <BR>note that this is NOT a 'hashCode', although because its a string a hashCode can be made from it, which is how the hashing works...<BR>
     * Once calculated upon instantiation, this value cannot be changed.
     */
    private String hashKey = null;
    /**
     * roleId for this task.
     * <BR>matches field in DB: tms.tasks.role_id.
     * Each Task must be in exactly 1 Role. Users are members of roles to either read, write or administer them.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private int roleId;
    /**
     * this Task is of type type.  Types are 'd' for deleted, 'x' for execute, 'e' for external, etc.
     * <BR>matches field in DB: tms.tasks.type.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private char type;
    /**
     * Tasks are only writable if the User has write access to them.  Otherwise they're read only.
     * <BR>user who changed/changing the status of this task.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private User owner = null;
    /**
     * start time field from the db.  This is null if this task has no status in the db.
     * <BR>matches field in DB: tms.task_states.start_time.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private java.sql.Time startTime = null;
    /**
     * end time field from the db.  This is null if this task has no status in the db, or if the task is still running, or other error.
     * <BR>matches field in DB: tms.task_states.end_time.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private java.sql.Time endTime = null;
    /**
     * current oracle time.
     * <BR>matches 'sysdate' in db.<BR>
     * This field cannot be null upon instantiation,
     * Once instantiated, this value cannot be changed.
     */
    private java.sql.Time currentTime = null;
    /**
     * StatusId of this task (maps to a status name in the tms.task_statuses table).
     * <BR>matches field in DB: tms.task_states.status_id<BR>
     * A status represents the state that a Task is in, for example 'running', 'not yet run', 'success' etc.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private int statusId;
    /**
     * Tasks are grouped into categories for display and processing.  At this time, the categories are only used for display.
     * <BR><BR>In the deamon in perl, the categories are more explicit but the same concept applies.  <BR>
     * Here categories are "completed", "stopped on error" "waiting for user" etc.  <BR>
     * In the deamon an example of a category is "active_tasks".<BR>
     * Categories are determined based on the statusId of the Task and its task type. <BR>
     * Every time ordering is changed this must be changed too.
     */
    protected String category = null;
    /**
     * a background attribute that simply determines what order the categories are displayed in.
     * <BR><BR>every time category is changed this must be changed as well.
     */
    protected Integer ordering = null;
    /**
     * The username of the latest status update for this Task.
     * <BR><BR>matches field in DB: tms.task_states.source<BR>
     * If this Task has no status, then this field is null.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private String source = null;
    /**
     * The host machine from where this status was run.
     * <BR><BR>matches field in DB: tms.task_states.host<BR>
     * If this Task has no status, then this field is null.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private String host = null;
    /**
     * Any comment (or note) entered in for this status.
     * <BR><BR>matches field in DB: tms.task_states.note<BR>
     * If this Task has no status, then this field is null.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private String note = null;
    /**
     * This Task's description.  Descriptions are written with each task (they do not depend on the task status).
     * <BR><BR>matches field in DB: tms.tasks.description<BR>
     * Using the description field in the db is a *good thing*.  Otherwise the only clear record of what the Task does is in its taskTag.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private String description = null;
    /**
     * The command that is executed when this Task is run.
     * <BR><BR>matches field in DB: tms.tasks.command<BR>
     * Sometimes the command can be a path to a file, either as a url or an absolute unix file path.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private String command = null;
    /**
     * Date on the server, NOT oracle when this Task object was instantiated.
     * <BR><BR>THIS IS A CONVENIENCE FIELD.  FOR ALL COMPARISONS OR FOR THE USER, USE THE FIELD currentTime INSTEAD; IT REPRESENTS ORACLE TIME.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private java.util.Date creationTime = null;
    /**
     * Date at which this Task is being tracked; the "run_date" chosen by the user.
     * <BR><BR>The time field is meaningless.  The day/month/year is the interesting piece of information.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private java.util.Date dateOfStatus = null;
    /**
     * iteration The iteration that this task is in this day's status.
     * <BR>First time task is run on a day iteration is 1, second time it is run in the same day it is 2, etc.
     * <BR>If value is 0, it has not been set.
     */
    private int iteration = 0;
    /**
     * The start time for this task in the day (only the time part of the field is relevant).
     * <BR><BR>matches field in DB: tms.tasks.start_time (not to be confused with tms.task_states.start_time)<BR>
     * If this task runs on a daily cron, this field is not null.  Otherwise it's null.
     * Once instantiated, this value cannot be changed.
     */
    private java.sql.Time cronStartTime = null;
    /**
     * timeout field in the db.
     * <BR><BR>Once instantiated, this value cannot be changed.
     */
    private float timeout = 0;
    /**
     * retries field in the db.  The number of times the deamon will retry something after failure before giving up.
     * <BR><BR>Once instantiated, this value cannot be changed.
     */
    private int retries = 0;
    /**
     * store the predecessor (String NOT Task) from the sql. map to Task when needed...
     * <BR><BR>matches field in DB: tms.tasks.predecessor_tag<BR>
     * Once this field is used to determine the predecessorTask, don't use it anymore.<BR>
     * Once instantiated, this value cannot be changed.
     */
    private String predecessor = null;
    /**
     * store the predecessor as a Task.
     * <BR><BR>Once this is determined based on the predecessor (String) and the job_tag, <BR>
     * only use this and not the String predecessor.
     */
    private Task predecessorTask = null;
    /**
     * Vector of Tasks that this Task depends on.
     * <BR><BR>While its called 'dependencies' here, in the db and in the daemon, the jsp pages show this field as 'predecessors'.
     */
    private Vector<Task> dependencies = null;
    /**
     * Vector of Tasks that rely on this Task.
     * <BR><BR>This field is determined when the tree is built in TaskBuilder; it is not marked explicitly in the db, and the daemon doesn't bother figuring it out.  <BR>
     * It provides a way of walking down the tree which is used for recursive status changes like 'retry' and viewing children of this Task.<BR>
     * This field is called 'children' throughout this java front-end, but the jsp pages call it 'successor'
     */
    private Vector<Task> children = null;
    /**
     * The collection to which this task belongs.
     * <BR>Referenced for SORT_BY variable when sorting.
     */
    private TaskCollection tc = null;
    /** the start time for this task the last time that this Task ran */
    //private java.sql.Time lastStartTime = null;
    /** the end time for this task the last time that this Task ran */
    //private java.sql.Time lastEndTime = null;
    /**
     * the elapsed time in minutes for this task the last time that this Task ran.
     * <BR><BR>
     * May be the avg over its life span or some timespan as done by the sql in TaskBuilder.
     */
    private float lastElapsedTime = -1;

    /**
     * Only constructor for Tasks.
     * <BR><BR>Takes almost all data members stored in Tasks.  Tasks are ONLY instantiated from TaskBuilder.<BR>
     * <p/>
     * The following params cannot be null:<BR>
     * <ul><li>taskTag - the name of this task
     * <li>jobTag - the job that this task is in
     * <li>dateOfStatus - the date that this task's status is valid for.</ul>
     *
     * @param taskTag       field from db from TaskBuilder
     * @param jobTag        field from db from TaskBuilder
     * @param tc            the TaskCollection to which this Task belongs
     * @param roleId        field from db from TaskBuilder
     * @param type          field from db from TaskBuilder
     * @param owner         The User who instantiated this Task object (owns it)
     * @param startTime     field from db from TaskBuilder
     * @param endTime       field from db from TaskBuilder
     * @param currentTime   sysdate from db
     * @param dateOfStatus  run date that the status (or lack of status) for which the status of this Task is valid.
     * @param iteration     The iteration that this task is in this day's status.  First time task is run on a day iteration is 1, second time it is run in the same day it is 2, etc.
     * @param statusId      field from db from TaskBuilder
     * @param category      category this Task is in (running, stopped on error, completed, etc)
     * @param ordering      utility field used to order the categories in viewTasks.jsp
     * @param source        field from db from TaskBuilder
     * @param host          field from db from TaskBuilder
     * @param note          field from db from TaskBuilder
     * @param description   field from db from TaskBuilder
     * @param command       field from db from TaskBuilder
     * @param predecessor   field from db from TaskBuilder
     * @param cronStartTime field from db from TaskBuilder
     * @param timeout       field from db from TaskBuilder
     * @param retries       field from db from TaskBuilder
     * @throws InvalidTaskException Thrown if a field that cannot be null is null (taskTag, jobTag, dateOfStatus)
     */
    public Task(String taskTag, String jobTag, int roleId, char type, String description, String command, java.sql.Time cronStartTime,
                float timeout, int retries)
    {
        this.cronStartTime = cronStartTime;
        this.taskTag = taskTag;
        this.jobTag = jobTag;
        this.hashKey = jobTag + ":" + taskTag;
        this.roleId = roleId;
        this.type = type;
        this.description = description;
        this.command = command;
        this.timeout = timeout;
        this.retries = retries;
    }

    protected Task(String taskTag, String jobTag, TaskCollection tc, int roleId, char type, User owner,
                   java.sql.Time startTime, java.sql.Time endTime, java.sql.Time currentTime,
                   java.util.Date dateOfStatus, int iteration, int statusId, String category, Integer ordering, String source, String host,
                   String note, String description, String command, String predecessor, java.sql.Time cronStartTime,
                   float timeout, int retries)
    {
        if (taskTag == null || jobTag == null || dateOfStatus == null)
        {
            throw new InvalidTaskException("taskmaster.job.Task cannot be instantiated when any of these are null: taskTag jobTag dateOfStatus");
        }
        this.taskTag = taskTag;
        this.jobTag = jobTag;
        this.hashKey = jobTag + ":" + taskTag;
        this.tc = tc;
        this.roleId = roleId;
        this.type = type;
        this.owner = owner;
        this.startTime = startTime;
        this.endTime = endTime;
        this.currentTime = currentTime;
        this.dateOfStatus = dateOfStatus;
        this.iteration = iteration;
        this.statusId = statusId;
        this.category = category;
        this.ordering = ordering;
        this.source = source;
        this.host = host;
        this.note = note;
        this.description = description;
        this.command = command;
        this.predecessor = jobTag + ":" + predecessor;
        this.cronStartTime = cronStartTime;
        this.timeout = timeout;
        this.retries = retries;
        this.dependencies = new Vector<Task>();
        this.children = new Vector<Task>();
        this.creationTime = new java.util.Date();
    }

    /**
     * Update the data members of this task that reflect its status, but not the ones that don't change.
     * <BR><BR>Called ONLY from TaskBuilder, this method allows for all the statuses that determine all aspects of this Task's state,<BR>
     * such as category, statusId, etc to be reset from when this Task was instantiated.  This provides a way to alter everything
     * in a Task that could change, without having to instantiate a new Task.
     *
     * @param startTime   field from db from TaskBuilder
     * @param endTime     field from db from TaskBuilder
     * @param currentTime sysdate
     * @param statusId    field from db from TaskBuilder
     * @param category    category this Task is in (running, stopped on error, completed, etc)
     * @param ordering    utility field used to order the categories in viewTasks.jsp
     * @param source      field from db from TaskBuilder
     * @param host        field from db from TaskBuilder
     * @param note        field from db from TaskBuilder
     */
    protected void update(java.sql.Time startTime, java.sql.Time endTime, java.sql.Time currentTime,
                          int statusId, String category, Integer ordering, String source, String host,
                          String note)
    {
        this.startTime = startTime;
        this.endTime = endTime;
        this.currentTime = currentTime;
        this.statusId = statusId;
        this.category = category;
        this.ordering = ordering;
        this.source = source;
        this.host = host;
        this.note = note;
    }

    protected void setLastRunDuration(float elapsedTime)
    {
        this.lastElapsedTime = elapsedTime;
    }

    /**
     * allows read access to the taskTag field for this task from the db.
     * <BR><BR>each Task belongs to exactly 1 job.  Within a job, each task must have a unique name, represented by this field.<BR>
     * Task tag is one of the few things that can't be null, as its used to identify the Task.<BR>
     *
     * @return the name of this Task.
     * @see getJobTag
     */
    public String getTaskTag()
    { return this.taskTag; }

    /**
     * allows read access to the jobTag field for this task from the db.
     * <BR><BR>each Task belongs to exactly 1 job, represented by this string.<BR>
     * Job tag is one of the few things that can't be null, as its used to identify the Task.<BR>
     *
     * @return the job name to which this Task belongs.
     * @see getTaskTag
     */
    public String getJobTag()
    { return this.jobTag; }

    /**
     * returns a string that represents this task and is guaranteed to be unique amongst any other Tasks in tms.
     * <BR><BR>Nothing too fancy, just [jobTag]:[taskTag].<BR>
     * This is used when hashing Tasks as the key for the hashtable.<BR>
     *
     * @return [jobTag]:[taskTag]
     */
    public String getKey()
    { return this.hashKey; }

    /**
     * allows read access to the roleId field for this task.
     * <BR><BR>matches field in DB: tms.tasks.role_id<BR>
     * Each Task must be in exactly 1 Role. Users are members of roles to either read, write or administer them.<BR>
     *
     * @return the role to which this Task belongs
     */
    public int getRoleId()
    { return this.roleId; }

    /**
     * allows read access to the type field for this task.
     * <BR><BR>Types are 'd' for deleted, 'x' for execute, 'e' for external, etc.<BR>
     * matches field in DB: tms.tasks.role_id.
     * <BR><BR>
     * ViewTasks.getTypeName will map a task's task type to its full name, instead of just this char.<BR>
     *
     * @return this Task's task type, as a char.
     * @see taskmaster.controller.ViewTasks.getTypeName
     */
    public char getType()
    { return this.type; }

    /**
     * looks to see if the passed task_type is the same as this Task's type.
     * <BR><BR>The taskType char can be mapped to its full name (which is better than a single letter) with the getTypeName() method in ViewTasks.<BR>
     *
     * @param type a char (upper case A-Z) that potentially matches the task type of this Task.
     * @return true if the passed type == this Task's type.
     * @see taskmaster.controller.ViewTasks.getTypeName
     */
    public boolean isOfType(char type)
    {
        return (this.type == type) ? true : false;
    }

    /**
     * Checks the owner of this Task to see if they have read, write or admin access to it.
     * <BR><BR>A task is writable if and only if its owner (a user) has 'admin' access to the role in which this Task is a member.<BR><BR>
     *
     * @return true if the owner of this Task has admin access to it; false otherwise (read or write only access).
     * @see isWritable
     */
    public boolean isAdminable()
    {
        try
        {
            String accessLevel = this.owner.isInRole(this.roleId);
            return (accessLevel == null || !accessLevel.equals("admin")) ? false : true;
        } catch (Exception e)
        {
            System.err.println(this.getClass().getName() + ":isAdminable() (ignorable exception): " + e);
            return false;
        }
    }

    /**
     * Checks the owner of this Task to see if they have read, write or admin access to it.
     * <BR><BR>A task is writable if and only if its owner (a user) has 'write' or 'admin' access to the role in which this Task is a member.<BR>
     *
     * @return true if the owner of this task has write or admin access to it; false otherwise (read only)
     * @see isAdminable
     */
    public boolean isWritable()
    {
        try
        {
            String accessLevel = this.owner.isInRole(this.roleId);
            return (accessLevel == null || accessLevel.equals("read")) ? false : true;
        } catch (Exception e)
        {
            System.err.println(this.getClass().getName() + ":isWritable() (ignorable exception): " + e);
            return false;
        }
    }

    /**
     * user who to whom this Task was instantiated for.  Each task in a tree will have the same owner.
     *
     * @return The user who owns this Task
     */
    public User getOwner()
    { return this.owner; }

    /**
     * allows read access to the start time field for this task from the db.
     * <BR><BR>This is null if this task has no status in the db.<BR>
     * Tasks that have statuses always have start_times, but they may not have end_times.<BR>
     * matches field in DB: tms.task_states.start_time<BR>
     *
     * @return time that this Task started as a java.sql.Date
     */
    public java.sql.Time getStartTime()
    { return this.startTime; }

    /**
     * Allows read access to the end time field for this task from the db.
     * <BR><BR>This is null if this task has no status in the db or if its still running or for some other reason the Task has no end time.<BR>
     * matches field in DB: tms.task_states.end_time.<BR>
     *
     * @return time that this Task ended as a java.sql.Date
     */
    public java.sql.Time getEndTime()
    { return this.endTime; }

    /**
     * same as getFormattedElapsedTime except it returns a java.util.Date object and not a string.
     * <BR><BR>The date (month, day, year) part of this is wrong.  Only the time (hours, minutes, seconds) is correct.<BR>
     * Lots of comments for getFormattedElapsedTime; look there for more info!<BR>
     *
     * @return same as getFormattedElapsedTime except as a java.util.Date object, not a string
     * @see getFormattedElapsedTime
     */
    public java.util.Date getElapsedTime()
    {
        if (this.startTime == null || (this.currentTime == null && this.endTime == null))
        {
            return null;
        }
        if (this.endTime != null)
        {
            return new java.util.Date(this.endTime.getTime() - this.startTime.getTime());
        }
        return new java.util.Date(this.currentTime.getTime() - this.startTime.getTime());
    }

    /**
     * returns a java.util.Date object whose time portion represents the elapsed time of the task the last time the task ran to a status considered by the db to be 'complete'.
     * <BR><BR>
     * Returns null if either the lastEndTime or the lastStartTime is null.
     * <BR>
     * The date (month, day, year) part of this is wrong.  Only the time (hours, minutes, seconds) is correct.<BR>
     * Lots of comments for getFormattedElapsedTime; look there for more info!<BR>
     *
     * @see getFormattedElapsedTime
     * @see getElapsedTime
     */
    public float getLastRunElapsedTime()
    {
        return this.lastElapsedTime;
    }

    /**
     * returns an estimate percentage done for this task, or -1 if no data exists.
     * <BR><BR>
     * Darrell 021220 - fixed occurance when there's no historical run data for this Task.  returns -1 to indicate no data available.
     */
    public float getPercentFinishedVsLast()
    {
        final Date elapsedTime = this.getElapsedTime();
        if (this.lastElapsedTime == -1 || elapsedTime == null)
        // hasn't been set; no old run times in db
        {
            return -1;
        }
//        System.err.println("QWERWER: " + this.lastElapsedTime);

        return ((float) (elapsedTime.getTime()) / (float) (this.lastElapsedTime * 60 * 1000)) * 100;
    }

    /**
     * If a task has no status, then null is returned as there is no elapsed time.
     * <BR><BR>If a task is running, elapsed time is determined based on difference between Oracle time and the start time of this Task.<BR>
     * If a task has a status and is not running (completed, failed, etc) elapsed time is determined based on the difference between start time and end time.<BR>
     * Oracle time is determined by simple 'sysdate' at time of query when this Task is instantiated.<BR>
     * Theoretically accurate to the millisecond, but I dunno if that's really true.<BR><BR>
     * <p/>
     * The time portion of this Date is interesting.  The date (month, day, year) are returned as seconds since the epoch way back when in 1970.<BR>
     * If tasks are run >24 hours, then this will break. Since the formatting (HH:mm:SS) doesn't display the date, it wouldn't make sense anyway.<BR>
     * <BR>
     * There is a bug someplace between the oracle driver that seems to set the oracle time to GMT.<BR>
     * While java.util.Date objects inherit their locale (correctly New York), these dates don't, so there's a time zone offset of 5 hours.<BR>
     * Whenever one of the other times is compared to this oracle time, it is thus off by 5 hours.<BR>
     * So, when it should say elapsed time of 5 minutes, it ends up saying elapsed time of 19 hours, 5 minutes the previous day.<BR>
     * To correct this, since its a total mystery to me why this works this way, comparisons are re-offset by this 5 hours (adds an extra 1000*60*60*5 milliseconds).<BR>
     *
     * @return The elapsed time that this task has been running or ran for in the format HH:mm:ss (hours:minutes:seconds)
     */
    public String getFormattedElapsedTime()
    {
        java.util.Date time = null;
//		int GMT_OFFSET = 1000*60*60*5; save time in the calculation!
        int GMT_OFFSET = 18000000;
        if (this.startTime == null || (this.currentTime == null && this.endTime == null))
        {
            return null;
        }
        if (this.endTime != null)
        {
            time = new java.util.Date(this.endTime.getTime() - this.startTime.getTime() + GMT_OFFSET);
        } else
        {
            time = new java.util.Date(this.currentTime.getTime() - this.startTime.getTime() + GMT_OFFSET);
        }

        java.text.SimpleDateFormat s = new java.text.SimpleDateFormat("HH:mm:ss");
        return s.format(time, new StringBuffer(), new java.text.FieldPosition(0)).toString();
    }

    /**
     * allows read access to the statusId field for this task from the db.
     * <BR><BR>matches field in DB: tms.task_states.status_id<BR>
     * A status represents the state that a Task is in, for example 'running', 'not yet run', 'success' etc.<BR>
     *
     * @return StatusId of this task
     * @see taskmaster.controller.ViewTasks.findSpecificStatusName
     */
    public int getStatusId()
    { return this.statusId; }

    /**
     * allows read access to the source field for this task from the db.
     * <BR>represents the user who entered this status into the db<BR>
     * matches field in DB: tms.task_states.source<BR>
     * If this Task has no status, then this field is null.
     *
     * @return The username of the latest status update for this Task.
     */
    public String getSource()
    { return this.source; }

    /**
     * allows read access to the host field for this task from the db.
     * <BR>The host machine from where this status was run.<BR>
     * matches field in DB: tms.task_states.host<BR>
     * If this Task has no status, then this field is null.<BR>
     *
     * @return The host machine from where this status was run.
     */
    public String getHost()
    { return this.host; }

    /**
     * allows read access to the note field for this task from the db.
     * <BR>Any comment (or note) entered in for this status; an explanation of why it went to this status.<BR>
     * If this Task has no status, then this field is null.<BR>
     *
     * @return matches field in DB: tms.task_states.note
     */
    public String getNote()
    { return this.note; }

    /**
     * allows read access to the description field for this task from the db.
     * <BR>Descriptions are written with each task (they do not depend on the task status).<BR>
     * matches field in DB: tms.tasks.description<BR>
     * Using the description field in the db is a *good thing*.  Otherwise the only clear record of what the Task does is in its taskTag.<BR>
     *
     * @return This Task's description.
     */
    public String getDescription()
    { return this.description; }

    /**
     * allows read access to the command field for this task from the db.
     * <BR>matches field in DB: tms.tasks.command<BR>
     * Sometimes the command can be a path to a file, either as a url or an absolute unix file path.<BR>
     *
     * @return The command that is executed when this Task is run.
     */
    public String getCommand()
    { return this.command; }

    /**
     * Date on the server, NOT oracle when this Task object was instantiated.
     * <BR>THIS IS A CONVENIENCE FIELD.  FOR ALL COMPARISONS OR FOR THE USER, USE THE FIELD currentTime INSTEAD; IT REPRESENTS ORACLE TIME.<BR>
     *
     * @return Date/Time of the server, not Oracle.
     */
    public java.util.Date getCreationTime()
    { return this.creationTime; }

    /**
     * Date at which this Task is being tracked; the "run_date" chosen by the user.
     * <BR>The time field is meaningless.  The day/month/year is the interesting piece of information.<BR>
     *
     * @return Date at which this Task is being tracked; the "run_date" chosen by the user.
     */
    public java.util.Date getDateOfStatus()
    { return this.dateOfStatus; }

    /**
     * returns the iteration for this task.
     * <BR>The first time a task is run in a day its iteration is 1, the second time in that same day it is 2, etc.
     *
     * @return this task's iteration. Default value is 0, so if that comes back it probably hasn't been set.
     */
    public int getIteration()
    { return this.iteration; }

    /**
     * allows read access to the start_time field (in tms.tasks NOT tms.task_statuses) for this task from the db.
     * <BR>the time of the day that the TMS daemon will start this task.<BR>
     * Only the time field is relevant.  The date portion is meaningless.<BR>
     * java.sql.Time is a wrapper class for java.util.Date.<BR>
     *
     * @return java.sql.Time (NOT java.util.Date, though a wrapper class) for when this Task will be fired off by the TMS daemon.
     */
    public java.sql.Time getCronStartTime()
    { return this.cronStartTime; }

    /**
     * allows read access to the timeout field for this task from the db.
     * <BR>The amount of time tms daemon will wait while running before marking this task as failed.<BR>
     * A value of 0 means no timeout (the most common) and will show up in the jsp as 'None'.<BR>
     *
     * @return tms.tasks timeout field for this Task
     */
    public float getTimeout()
    { return this.timeout; }

    /**
     * allows read access to the retries field for this task from the db.
     * <BR>the field in the db that defines the number of times this Task will be retried to be run by the tms daemon upon failure before giving up.<BR>
     *
     * @return tms.tasks table, retries field for this Task.
     */
    public int getRetries()
    { return this.retries; }

    /**
     * Once the tree is built, getPredecessorTask should be called instead of this method.
     * <BR>This only returns the TaskTag as a string of the predecessor.<BR>
     * allows read access to the predecessor field for this task from the db.<BR>
     *
     * @return The STRING representation of the predecessor of this Task.
     * @see getPredecessorTask
     */
    protected String getPredecessor()
    { return this.predecessor; }

    /**
     * Each task can have up to 1 predecessor (opposite of child).
     * <BR>Called when building the tree.<BR>
     *
     * @param pred The Task (in the same job as this Task) that is the predecessor of this Task in the tree.  Can be null.
     * @see getPredecessor
     */
    public void setPredecessor(Task pred)
    {
        this.predecessorTask = pred;
    }

    /**
     * The task, if there is one, that this Task thinks about as its Predecessor (thread).
     *
     * @return the Task that this Task thinks of as its parent in the thread.  Null if there isn't one.
     * @see setPredecessor
     */
    public Task getPredecessorTask()
    { return this.predecessorTask; }

    /**
     * Adds this child Task to the internal vector of all this Task's children.
     * <BR>Each task can have any number of children (opposite of dependencies).<BR>
     * Since any number of child Tasks can point to a single parent Task as their predecessor,
     * predecessors of a child Task are grouped with the children of the parent Task.<BR>
     * So, the opposite of a dependency is a child, and the opposite of a predecessor is also a child because there can be more than 1.<BR>
     * Called when building the tree in TaskBuilder.<BR>
     *
     * @param child A Task that thinks of this Task as either a dependency or a predecessor.
     * @see getAllChildren
     * @see taskmaster.job.TaskBuilder
     */
    public void addChild(Task child)
    {
        this.children.add(child);
    }

    /**
     * Provides the ability to walk down the tree.
     * <BR><BR>Since a parent Task can have two children Task's that both think of it as a predecessor,<BR>
     * There is no concept of the predecessor Task when going down the tree.<BR>
     * Thus, unlike getAllDependencies() & getPredecessor() this list mixes all child Tasks that think of this Task as their parent either as a predecessor or as a dependency.<BR>
     * the opposite of a dependency is a child, and the opposite of a predecessor is also a child because there can be more than 1.<BR>
     * This is NOT recursive down the tree.<BR>
     *
     * @return the immediate children of this Task.
     * @see getAllDependencies
     * @see getPredecessor
     * @see addChild
     */
    public TaskEnumeration getAllChildren()
    { return new TaskEnumeration(this.children.elements()); }

    /**
     * An internal vector representing this Task's immediate dependencies.
     * <BR>This list is for dependencies only, not predecessors.<BR>
     * Each task can have any number of dependencies<BR>
     *
     * @param dependency A Task that this Task is dependant upon before it can run.  Referred to as a 'parent' by TaskBuilder.
     * @see setPredecessor
     */
    public void addDependency(Task dependency)
    {
        this.dependencies.add(dependency);
    }

    /**
     * Simply turns the internal vector representing all of this Task's immediate.
     * <BR>dependencies into a TaskEnumeration.<BR>
     * This Enumeration does not include this Task's predecessor, if it has one.<BR>
     * This is NOT recursive up the tree.
     *
     * @return a TaskEnumation of all the immediate dependencies of this Task
     * @see taskmaster.util.TaskEnumeration
     * @see getPredecessorTask
     * @see getAllChildren
     */
    public TaskEnumeration getAllDependencies()
    { return new TaskEnumeration(this.dependencies.elements()); }

    /**
     * go through this Task's pred and dep list looking for any that return false to isCompleted().
     * <BR>This is not recursive up the tree; it just goes for the direct pred and direct deps.<BR>
     * <BR><BR>
     * August 19, 2002 - put in a missing "!" to the question of whether a checked dependency is in fact completed.
     * This method now correctly returns 'false' if there are any direct (not recursive) dependencies that are NOT completed.
     * The check for completed predecessors works and wasn't changed.
     *
     * @return true if all of this Task's immediate dependencies and predecessors are completed; false otherwise.
     */
    public boolean depsPredsCompleted()
    {
        if (this.predecessorTask != null && !this.predecessorTask.isCompleted())
        {
            return false;
        }
        int size = this.dependencies.size();
        for (int i = 0; i < size; i++)
        {
            if (!(this.dependencies.elementAt(i)).isCompleted())// added !
            {
                return false;
            }
        }
        return true;
    }

    /**
     * Is this task in the 'Stopped on Error' category?  checks the 'category' field.
     * <BR>if this task finished with status acceptable to satisfy its deps.  "succeeded" "skip" etc<BR>
     *
     * @return true if this task is completed; false otherwise
     */
    public boolean isCompleted()
    {
        return "completed".equals(this.category);
    }

    /**
     * Is this Task in the 'Stopped on Error' category? checks the internal 'category' field.
     *
     * @return true if this Task is currently running in tms someplace according to the db; false otherwise
     */
    public boolean isRunning()
    {
        return "running".equals(this.category);
    }

    /**
     * is this task in the 'Stopped on Error' category?  checks the 'category' field.
     *
     * @return true if this task is stopped on any type of error; false otherwise
     */
    public boolean stoppedOnError()
    {
        return "error".equals(this.category);
    }

    /**
     * is this task in the 'Waiting on User' category?  checks the 'category' field.
     *
     * @return true if all deps and pred are completed, and its not completed or running; false otherwise
     */
    public boolean isWaitingOnUser()
    {
        return "needsUser".equals(this.category);
    }

    /**
     * is this task a cron (actually something with a crontab)?  checks the 'category' field.
     * Cron types are OBSOLETE as a type.  They are used internally to jtms, however, to denote Tasks with time dependencies, which is how TMS does cron.<BR>
     *
     * @return true if this task is a cron; false otherwise
     * @throws Exception not thrown under any known conditions
     */
    public boolean isCron() throws Exception
    {
        return this.category.equals("cron");
    }

    /**
     * is this task in the Blocked category?  checks the 'category' field.
     *
     * @return true if task is blocked by another; false otherwise
     * @throws Exception not thrown under any known conditions
     */
    public boolean isBlocked() throws Exception
    {
        return this.category.equals("blocked");
    }

    /**
     * returns the internal category string used to determine the category that this task is in.
     * <BR><BR>This is NOT good for display as its a terse string.<BR>
     * It is used in the loop in viewTasks.jsp to figure out if the next task is in the loop
     * is in a different category, in which case it displays the appropriate category separator.
     *
     * @return internal category string
     * @throws Exception not thrown under any known conditions
     * @see isWaitingOnUser#
     * @see stoppedOnError#
     * @see isCompleted#
     * @see isRunning#
     * @see isCron#
     */
    public String getQuickCategory()
    {
        return this.category;
    }

    /**
     * gets a displayable string denoting the category that this Task is in.
     * <BR><BR>Used, for example, for dependancies on the right-hand column of viewTasks.jsp.<BR>
     * If you want that column to show up differently, like have it say "where is the user?" where it would now say "Waiting on user", that is changed here.
     *
     * @return Displayable string denoting the category that this Task is in.
     * @throws Exception isn't thrown under any known circumstances
     * @see isWaitingOnUser
     * @see stoppedOnError
     * @see isCompleted
     * @see isRunning
     * @see isCron
     */
    public String getCategory()
    {
        if (isWaitingOnUser())
        {
            return "Waiting on user";
        }
        if (stoppedOnError())
        {
            return "Stopped on error";
        }
        if (isCompleted())
        {
            return "Completed";
        }
        if (isRunning())
        {
            return "Running now";
        }
//        if (isCron())
//        {
//            return "Wait until " + this.getCronStartTime();
//        }
//		if (isBlocked(task))
//			return "Blocked";
//		return "Not yet run";
        return "Blocked";
    }

    /**
     * overrides default method.
     * <BR><BR>Compare this task to passed Task based on what was set by setSortOrder().<BR>
     * Defaults to ordering by TASK_TAG<BR>
     *
     * @param o Must be castable as a Task, or else ClassCastException is thrown
     * @return int expected by sort (-1, 0, 1) depending on what the set SORT_ORDER is for this Task.
     */
    public int compareTo(Object o)
    {
        Task task = (Task) o;
        switch (this.tc.ORDER_BY)
        {
            case TaskCollection.ORDER_BY_TASK_TAG:
                return orderByTaskTag(task, true);
            case TaskCollection.ORDER_BY_JOB_TAG:
                return orderByJobTag(task, true);
            case TaskCollection.ORDER_BY_WRITABLE:
                return orderByWritable(task, true);
            case TaskCollection.ORDER_BY_START_TIME:
                return orderByStartTime(task, true);
            case TaskCollection.ORDER_BY_END_TIME:
                return orderByEndTime(task, true);
            case TaskCollection.ORDER_BY_ELAPSED_TIME:
                return orderByElapsedTime(task, true);
            default:
                return orderByTaskTag(task, true);
        }
    }

    /**
     * order by this field
     */
    private int orderByTaskTag(Task task, boolean categorize)
    {
        if (categorize && this.ordering.compareTo(task.ordering) != 0)
        {
            return this.ordering.compareTo(task.ordering);
        }
        return this.taskTag.compareTo(task.taskTag);
    }

    /**
     * order by this field
     */
    private int orderByJobTag(Task task, boolean categorize)
    {
        if (categorize && this.ordering.compareTo(task.ordering) != 0)
        {
            return this.ordering.compareTo(task.ordering);
        }
        int i = 0;
        if ((i = this.jobTag.compareTo(task.jobTag)) != 0)
        {
            return i;
        }
        return this.taskTag.compareTo(task.taskTag);
    }

    /**
     * order by this field
     */
    private int orderByWritable(Task task, boolean categorize)
    {
        if (categorize && this.ordering.compareTo(task.ordering) != 0)
        {
            return this.ordering.compareTo(task.ordering);
        }
        return (this.isWritable() && task.isWritable()) ? 0 : this.taskTag.compareTo(task.taskTag);
    }

    /**
     * order by this field
     */
    private int orderByStartTime(Task task, boolean categorize)
    {
        if (categorize && this.ordering.compareTo(task.ordering) != 0)
        {
            return this.ordering.compareTo(task.ordering);
        }
        if (this.startTime == null && task.startTime == null)
        {
            return this.taskTag.compareTo(task.taskTag);
        }
        if (this.startTime == null)
        {
            return 1;
        }
        if (task.startTime == null)
        {
            return -1;
        }
        return this.startTime.compareTo(task.startTime);
    }

    /**
     * order by this field
     */
    private int orderByEndTime(Task task, boolean categorize)
    {
        if (categorize && this.ordering.compareTo(task.ordering) != 0)
        {
            return this.ordering.compareTo(task.ordering);
        }
        if (this.endTime == null && task.endTime == null)
        {
            return this.taskTag.compareTo(task.taskTag);
        }
        if (this.endTime == null)
        {
            return 1;
        }
        if (task.endTime == null)
        {
            return -1;
        }
        return this.endTime.compareTo(task.endTime);
    }

    /**
     * order by this field
     */
    private int orderByElapsedTime(Task task, boolean categorize)
    {
        if (categorize && this.ordering.compareTo(task.ordering) != 0)
        {
            return this.ordering.compareTo(task.ordering);
        }
        if (this.getElapsedTime() == null && task.getElapsedTime() == null)
        {
            return this.taskTag.compareTo(task.taskTag);
        }
        if (this.getElapsedTime() == null)
        {
            return 1;
        }
        if (task.getElapsedTime() == null)
        {
            return -1;
        }
        return this.getElapsedTime().compareTo(task.getElapsedTime());
    }

    /**
     * Overrides default toString with meaningful output.  Calls getKey().
     *
     * @return [taskTag]:[jobTag].
     */
    public String toString()
    {
        return this.getKey();
    }

    /**
     * USER CAREFULLY.  The integrity of the tree of Tasks in a TaskCollection relies should be kept free of other Tasks rolming around.  Could easily create confusion!!!
     * <BR><BR>Most of the uses of Task rely on pointing to the objects.<BR>
     * This method will copy (duplicate exactly) all members of this Task to a new location in memory.<BR>
     * At this time, this is used only in the case of checking for sync errors in updating Tasks.<BR>
     * See TaskCollection, where the updating is done for more details on that.
     *
     * @return A copy of this Task.  But NOT a copy of all its datamembers; only the static ones.
     * @throws CloneNotSupportedException just an implementation of the interface...  If its ever thrown there must have been added a complex datatype to Task which for some reason is being cloned.
     */
    public Object clone() throws CloneNotSupportedException
    {
        return super.clone();
    }
}

